package lumis.portal.authentication;

import java.security.SecureRandom;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.concurrent.atomic.AtomicInteger;

import javax.security.auth.login.LoginContext;
import javax.security.auth.login.LoginException;

import lumis.portal.PortalContext;
import lumis.portal.PortalException;
import lumis.portal.UnexpectedException;
import lumis.portal.authentication.event.SessionExpiredEvent;
import lumis.portal.authentication.event.SessionLoginEvent;
import lumis.portal.authentication.event.SessionLogoutEvent;
import lumis.portal.configuration.License;
import lumis.portal.dao.DaoFactory;
import lumis.portal.manager.ManagerException;
import lumis.portal.manager.ManagerFactory;
import lumis.portal.manager.ManagerStringResource;
import lumis.portal.user.IUserDao;
import lumis.portal.user.UserConfig;
import lumis.util.ITransaction;
import lumis.util.log.ILogger;
import lumis.util.log.LoggerFactory;

/**
 * Manager responsible for authenticating users and keeping session information. 
 * 
 * @since 4.0.0
 * @version $Revision: 15648 $ $Date: 2013-07-23 12:24:36 -0300 (Tue, 23 Jul 2013) $
 */
public class AuthenticationManager implements IAuthenticationManager
{
	private static ILogger logger = LoggerFactory.getLogger(AuthenticationManager.class);

	private ManagerStringResource authenticationStringResource;
		
	private char[] sessionIdChars = ("abcdefghijlkmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-").toCharArray();
	private SecureRandom random = new SecureRandom();
	private AtomicInteger sessionCountHash = new AtomicInteger();
	
	
	public AuthenticationManager()
	{
		authenticationStringResource = new ManagerStringResource();
		authenticationStringResource.addResourcePath("lumis/portal/authentication/strings/strings");
	}

	public boolean validateSessionId(SessionConfig sessionConfig, ITransaction transaction) throws ManagerException, PortalException
	{
		// impersonated SessionConfig validation is detected by the presence of userId in it
		if (sessionConfig.isImpersonated())
			return sessionConfig.getUserIdInternal() != null;

		// non-impersonated SessionConfig validation must be done checking the database
		try
		{
			String userId = DaoFactory.getAuthenticationDao().validateUserSession(sessionConfig.getUserSessionId(), transaction);
			if (userId == null)
				return false;
			else
			{
				sessionConfig.setUserId(userId);
				return true;
			}
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}

	/**
	 * Fetch the userId for a SessionConfig from the database and returns it.
	 * @param sessionConfig the SessionConfig.
	 * @param transaction the transaction for persistence access.
	 * @return the userId.
	 * @throws PortalException if it was not possible to obtain the userId.
	 * @since 4.0.8
	 */
	String getUserIdBySessionConfig(SessionConfig sessionConfig, ITransaction transaction) throws PortalException
	{
		return DaoFactory.getAuthenticationDao().getUserId(sessionConfig.getUserSessionId(), transaction);
	}
	
	@Deprecated
	public String getUserIdBySessionId(SessionConfig sessionConfig, ITransaction transaction) throws ManagerException, PortalException
	{
		return sessionConfig.getUserId();
	}

	@Deprecated
	public String loginFramworkClockUser(ITransaction transaction) throws ManagerException, PortalException
	{
		try
		{
			IAuthenticationDao authenticationDAO = DaoFactory.getAuthenticationDao();
			IUserDao userDao = DaoFactory.getUserDao();
			String userSessionId = generateSessionId();

			authenticationDAO.addUserSessionId(userSessionId, UserConfig.USER_FRAMEWORKCLOCK_ID);
			userDao.updateLastLogin(UserConfig.USER_FRAMEWORKCLOCK_ID, new Date());

			// push the SessionConfig instance in the current session stack
			SessionConfig sessionConfig = new SessionConfig(userSessionId, UserConfig.USER_FRAMEWORKCLOCK_ID, null, false);
			SessionConfig.getCurrentSessionStack(true).push(sessionConfig);
			
			return userSessionId;
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}

	@Deprecated
	public String loginUser(String login, String password, ITransaction transaction) throws ManagerException, PortalException
	{
		SessionConfig sessionConfig = login(login, password, transaction);
		if (sessionConfig != null)
			return sessionConfig.getUserSessionId();
		else
			return null;
	}

	public SessionConfig login(String login, String password, ITransaction transaction) throws ManagerException, PortalException
	{
		return login(PortalContext.getSecurityAlias(), login, password, transaction);
	}
	
	public SessionConfig login(String securityAlias, String login, String password, ITransaction transaction) throws ManagerException, PortalException
	{
		try
		{
			// will only perform password check if license is for full version
			if (License.isDevelopment())
			{
				LoginContext lc = null;
				if (securityAlias == null)
					securityAlias = PortalContext.getSecurityAlias();
				try
				{
					lc = new LoginContext(securityAlias, new LoginCallbackHandler(login, password));
				}
				catch (LoginException le)
				{
					logger.error("Could not instantiate LoginContext", le);
					return null;
				}
				catch (SecurityException se)
				{
					throw new ManagerException("STR_CREATE_LOGIN_CONTEXT_FAILED", authenticationStringResource);
				}
				try
				{
					lc.login();
				}
				catch (LoginException le)
				{
					logger.debug("Exception while calling LoginContext.login", le);
					return null;
				}
				// get the login name for the first principal
				for (java.security.Principal principal : lc.getSubject().getPrincipals())
				{
					login = principal.getName();
					break;
				}
			}
			
			String userId = DaoFactory.getUserDao().getUserIdByLogin(login, transaction);
			return loginUser(userId, transaction);
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}
	
	public SessionConfig loginUser(String userId, ITransaction transaction) throws PortalException
	{
		IUserDao userDao = DaoFactory.getUserDao();
		UserConfig userConfig = userDao.get(userId, transaction);

		if (userConfig.getType() == UserConfig.USER_TYPE_FRAMEWORK)
			throw new LoginFailedInvalidUserException("STR_CANNOT_LOGIN_FRAMEWORK_USER", authenticationStringResource);

		if (userConfig.isDisabled())
			throw new LoginFailedUserDisabledException("STR_CANNOT_LOGIN_USER_ACCOUNT_DISABLED", authenticationStringResource);

		if (!userConfig.isMultiSession())
			DaoFactory.getAuthenticationDao().deleteUserSessionByUserId(userId);

		// apply the changes in the database
		String userSessionId = generateSessionId();
		DaoFactory.getAuthenticationDao().addUserSessionId(userSessionId, userId);
		userDao.updateLastLogin(userId, new Date());
		
		// Tries to create a new session using the last session locale.
		SessionConfig sessionConfig;
		SessionConfig currentSessionConfig = SessionConfig.getCurrentSessionConfig();
		if(currentSessionConfig != null)
		{
			sessionConfig = new SessionConfig(userSessionId, userId, currentSessionConfig.getLocale(), false);
		}
		else
		{
			sessionConfig = new SessionConfig(userSessionId, userId, null, false);
		}
		// push the SessionConfig instance in the current session stack
		SessionConfig.getCurrentSessionStack(true).push(sessionConfig);
		
		if (logger.isDebugEnabled())
			logger.debug("User '" + userId + "' logged in with userSessionId '" + userSessionId + "'");
		
		// send notification of login event
		SessionLoginEvent event = new SessionLoginEvent(sessionConfig, Collections.singleton(sessionConfig), transaction);
		ManagerFactory.getPortalEventManager().notifyObservers(event);
			
		return sessionConfig;		
	}
	
	public void logoutUser(SessionConfig sessionConfig, ITransaction transaction) throws ManagerException, PortalException
	{
		try
		{
			// send notification of logout event
			SessionLogoutEvent event = new SessionLogoutEvent(sessionConfig, Collections.singleton(sessionConfig), transaction);
			ManagerFactory.getPortalEventManager().notifyObservers(event);
			
			// remove session from the database
			DaoFactory.getAuthenticationDao().deleteUserSessionId(sessionConfig.getUserSessionId());
			
			// remove the current sessionConfig from the currentSessionStack, if 
			// it has the same userSessionId as the given sessionConfig.
			SessionConfig currentSessionConfig = SessionConfig.getCurrentSessionConfig();
			if (currentSessionConfig != null && currentSessionConfig.getUserSessionId().equals(sessionConfig.getUserSessionId()))
				SessionConfig.getCurrentSessionStack(true).pop();
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}

	@Deprecated
	public String loginUserGuest() throws ManagerException, PortalException
	{
		String userSessionId = generateSessionId();
		DaoFactory.getAuthenticationDao().addUserSessionId(userSessionId, UserConfig.USER_GUEST_ID);

		// Tries to create a new session using the last session locale.
		SessionConfig sessionConfig;
		SessionConfig currentSessionConfig = SessionConfig.getCurrentSessionConfig();
		if(currentSessionConfig != null)
		{
			sessionConfig = new SessionConfig(userSessionId, UserConfig.USER_GUEST_ID, currentSessionConfig.getLocale(), false);	
		}
		else
		{
			sessionConfig = new SessionConfig(userSessionId, UserConfig.USER_GUEST_ID, null, false);	
		}
		// push the SessionConfig instance in the current session stack
		SessionConfig.getCurrentSessionStack(true).push(sessionConfig);
		
		return userSessionId;
	}

	public void clearInactiveSessions(SessionConfig sessionConfig, int minutes, ITransaction transaction) throws ManagerException, PortalException
	{
		Collection<SessionConfig> sessions = DaoFactory.getAuthenticationDao().clearInactiveSessions(minutes, transaction);
		
		if (!sessions.isEmpty())
		{
			// send sessions expiration notification
			SessionExpiredEvent event = new SessionExpiredEvent(sessionConfig, sessions, transaction);
			ManagerFactory.getPortalEventManager().notifyObservers(event);
		}
	}

	public void keepSessionAlive(SessionConfig sessionConfig) throws ManagerException, PortalException
	{
		try
		{
			DaoFactory.getAuthenticationDao().keepSessionAlive(sessionConfig.getUserSessionId());
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}
	
	public SessionConfig impersonate(String userId) throws PortalException
	{
		// create impersonation session config
		SessionConfig impersonation;
		SessionConfig currentSessionConfig = SessionConfig.getCurrentSessionConfig();
		if (currentSessionConfig != null)
			impersonation = new SessionConfig(null, userId, currentSessionConfig.getLocale(), true);
		else
			impersonation = new SessionConfig(null, userId, null, true);
		
		// apply the impersonation session config
		SessionConfig.getCurrentSessionStack(true).push(impersonation);
		
		return impersonation;
	}
	
	public void endImpersonation(SessionConfig sessionConfig) throws PortalException
	{
		// check if the given session config is the same instance as the 
		// current session config
		SessionConfig impersonation = SessionConfig.getCurrentSessionConfig();
		if (impersonation != sessionConfig)
			throw new IllegalArgumentException("The sessionConfig argument is not the current impersonated SessionConfig.");
		
		// remove the session config from the stack
		SessionConfig.getCurrentSessionStack(true).pop();
		
		// remove the userId from the session config to prevent it from being used
		impersonation.invalidate();
	}

	public Collection<SessionConfig> getSessionsByUserId(SessionConfig sessionConfig, String userId, ITransaction transaction) throws PortalException
	{
		try
		{
			return DaoFactory.getAuthenticationDao().getSessionsByUserId(userId, transaction);
		}
		catch (PortalException e)
		{
			throw e;
		}
		catch (Exception e)
		{
			throw new UnexpectedException(e);
		}
	}

	/**
	 * Clears {@link SessionConfig session config's} {@link Thread thread} data.
	 * 
	 * @since 6.0.0
	 */
	public void clearThreadData()
	{
		SessionConfig.clearThreadLocalData();
	}
	
	/**
	 * Generates a new session identifier. 
	 * Based on {@link SecureRandom}, incremental integer and current time.
	 * @return the generated session identifier.
	 * @since 7.0.2
	 */
	private String generateSessionId()
	{
		StringBuilder sessionId = new StringBuilder(32);		
		long currentTime = System.currentTimeMillis();
		int incrementalInteger = this.sessionCountHash.getAndIncrement();
		
		byte[] rnd = new byte[25];
		random.nextBytes(rnd);
		int rndIndex = 0;
		
		int step = 0;
		while(step<15)
		{
			switch (step)
			{
				case 1:
					sessionId.append(sessionIdChars[incrementalInteger & 63]);
					break;
				case 3:
					sessionId.append(sessionIdChars[(int) (currentTime & 63)]);				
					break;
				case 5:
					sessionId.append(sessionIdChars[(int) (currentTime>>6) & 63]);
					break;
				case 7:
					sessionId.append(sessionIdChars[(int) (currentTime>>12) & 63]);
					break;
				case 8:
					for(int i = 0;i<4;i++)
					{
						int integer = rnd[rndIndex++];
						sessionId.append(sessionIdChars[integer & 63]);
					}
					break;
				case 9:
					sessionId.append(sessionIdChars[(incrementalInteger>>6) & 63]);
					break;
				case 11:
					sessionId.append(sessionIdChars[(int) (currentTime>>18) & 63]);
					break;
				case 13:
					sessionId.append(sessionIdChars[(int) (currentTime>>24) & 63]);
					break;
				default:
					for(int i = 0;i<3;i++)
					{
						int integer = rnd[rndIndex++];
						sessionId.append(sessionIdChars[integer & 63]);
					}
					break;	
			}
			step++;
		}
		return sessionId.toString();
	}
}